vimfandomcom-20200223-history
Implement your own interactive finder without plugins
Although there are many (fuzzy)-finder plugins around—most notably CtrlP—you may find them overkill for your own use cases, too slow, too complicated to extend, or with too many requirements (like Vim compiled with Ruby or Python), etc… Or you would just like to avoid installing yet another plugin. Well, it turns out that implementing your own finder à la CtrlP is pretty easy. If you think “fuzzy imprecise” then you may like this solution. The core idea is extremely simple: # Populate a new scratch buffer with the items to be filtered, one item per line; # Use :global! to filter out items as the user types new characters; # Use Vim undo functionality to restore a previous state when the user presses backspace. The (fully functional and self-contained!) code below illustrates the idea. It allows you to interactively filter a list of items as you type, and return the selected item. You may use CTRL-K and CTRL-J to move up and down through the list; CTRL-L to clear the filter; ENTER to accept the current line; ESC to cancel. You may anchor the pattern at the start or at the end with ^ and $ respectively, and you can match any single character with . and any string with .*. For instance, ^v.*m will match Vim and vrooom!. The parameters are as follows: input: either a shell command that sends its output, one item per line, to stdout, or a List of items to be filtered. prompt: a String to be displayed at the command prompt. Dealing with a multiple selection is left as an exercise to the reader :-) fun! FilterClose(bufnr) wincmd p execute "bwipe" a:bufnr redraw echo "\r" return [] endf fun! Finder(input, prompt) abort let l:prompt = a:prompt . '>' let l:filter = "" let l:undoseq = [] botright 10new +setlocal\ buftype=nofile\ bufhidden=wipe\ \ nobuflisted\ nonumber\ norelativenumber\ noswapfile\ nowrap\ \ foldmethod=manual\ nofoldenable\ modifiable\ noreadonly let l:cur_buf = bufnr('%') if type(a:input) # v:t_string let l:input = systemlist(a:input) call setline(1, l:input) else " Assume List call setline(1, a:input) endif setlocal cursorline redraw echo l:prompt . " " while 1 let l:error = 0 " Set to 1 when pattern is invalid try let ch = getchar() catch /^Vim:Interrupt$/ " CTRL-C return FilterClose(l:cur_buf) endtry if ch # "\" " Backspace let l:filter = l:filter:-2 let l:undo = empty(l:undoseq) ? 0 : remove(l:undoseq, -1) if l:undo silent norm u endif elseif ch >=# 0x20 " Printable character let l:filter .= nr2char(ch) let l:seq_old = get(undotree(), 'seq_cur', 0) try " Ignore invalid regexps execute 'silent keepp g!:\m' . escape(l:filter, '~\. ':norm "_dd' catch /^Vim\%((\a\+)\)\=:E/ let l:error = 1 endtry let l:seq_new = get(undotree(), 'seq_cur', 0) " seq_new != seq_old iff the buffer has changed call add(l:undoseq, l:seq_new != l:seq_old) elseif ch # 0x1B " Escape return FilterClose(l:cur_buf) elseif ch # 0x0D " Enter let l:result = empty(getline('.')) ? [ : getline('.') call FilterClose(l:cur_buf) return l:result elseif ch # 0x0C " CTRL-L (clear) call setline(1, type(a:input) # v:t_string ? l:input : a:input) let l:undoseq = [] let l:filter = "" redraw elseif ch # 0x0B " CTRL-K norm k elseif ch # 0x0A " CTRL-J norm j endif redraw echo (l:error ? "pattern " : "").l:prompt l:filter endwhile endf The code can be used as follows: let items = Finder('one','two','three','four','five', 'Choose') echo "You have chosen: " empty(items) ? 'nothing' : items0 Here are a few possible uses: Buffer switcher: let buffers = split(execute('ls'), "\n") let choice = Finder(buffers, 'Switch to buffer') if !empty(choice) execute "buffer" split(choice0, '\s\+')0 endif Edit most recently used files (MRU): let paths = Finder(v:oldfiles, 'Choose file') if !empty(paths) execute "args" join(map(paths, 'fnameescape(v:val)')) endif Choose colorscheme: let colorschemes = map(globpath(&runtimepath, "colors/*.vim", 0, 1), \ 'fnamemodify(v:val, ":t:r")') let colorschemes += map(globpath(&packpath, \ "pack/*/{opt,start}/*/colors/*.vim", 0, 1), \ 'fnamemodify(v:val, ":t:r")') let choice = Finder(colorschemes, 'Choose colorscheme') if !empty(choice) execute "colorscheme" choice0 endif Find files in the current directory using find: let choice = Finder('find . -type f', "Choose file") if !empty(choice) execute "edit" choice0 endif Find files in the current directory using rg: let choice = Finder('rg --files .', "Choose file") if !empty(choice) execute "edit" choice0 endif Filter quickfix list and jump to item: let qfentry = Finder(split(execute('clist'), "\n"), 'Choose qf entry') if !empty(qfentry) execute "crewind" matchstr(qfentry0, '^\s*\d\+', )'' endif '''Find tag in current buffer (requires Exuberant Ctags):' let s:tags = Finder(systemlist('ctags -f - --sort=no --excmd=number ' \ . '--fields= --extra= --file-scope=yes ' \ . shellescape(expand('%'))), 'Choose tag') if !empty(tag) let s:bufname, s:line = split(s:tags0, '\s\+') execute "buffer" "+".s:line s:bufname endif Category:Finder Category:Filter Category:Interactive Category:Ctrlp Category:Mru Category:Buffer switching